Kapitel 10 Einige wichtige .NET-Klassen
10.1 Die Klasse »Object«  
Alle Klassen in der Klassenbibliothek des .NET-Frameworks sind Mitglieder einer Klassenhierarchie, die sich über viele Verzweigungen in aufgabenspezifischen Bereiche gliedert. Alle Klassen, so tief sie auch im Dickicht dieser Hierarchie stecken mögen, lassen sich aber auf die gemeinsame Basisklasse Object zurückführen. Entwickeln Sie eine benutzerdefinierte Klasse, müssen Sie nicht explizit angeben, dass Ihre Klasse von Object abgeleitet ist – diese Ableitung geschieht implizit. Dass sich alle Klassen von Object ableiten, hat eine ganz wesentliche Konsequenz: Jeder Typ des Systems weist ein Minimum gemeinsamer Verhaltensweisen auf.
10.1.1 Der Konstruktor  
Die Liste der Konstruktoren ist nicht sehr lang – es gibt nur einen, der parameterlos ist und von jeder abgeleiteten Klasse aufgerufen wird, wenn ein Objekt erstellt wird. Wie Sie wissen, erfolgt dieser Aufruf implizit.
10.1.2 Die Methoden der Klasse »Object«  
Insgesamt sieben Methoden vererbt die Klasse Object an ihre Subklassen. Fünf dieser Methoden sind Public und damit öffentlich, die beiden anderen Methoden Protected und werden daher zwar vererbt, erlauben aber nur den Zugriff aus der erbenden Klasse heraus.
Sehen wir uns zunächst in Tabelle 10.1 alle Methoden in einem Überblick an.
Tabelle 10.1 Die Methoden der Klasse »Object«
| Methoden
|
Beschreibung
|
| Equals
|
Diese Methode vergleicht zwei Objektreferenzen und liefert einen booleschen Wert zurück, dem entnommen werden kann, ob die beiden Referenzen auf dasselbe Objekt zeigen.
|
| Finalize
|
Dient dazu, Ressourcen der Klasse freizugeben, wenn das Objekt zerstört wird.
|
| GetHashCode
|
Liefert einen objektspezifischen Identifizierer.
|
| GetType
|
Liefert die Referenz auf eine Type-Instanz zurück, die den Typ des aktuellen Objekts beschreibt.
|
| MemberwiseClone
|
Dupliziert die aktuelle Instanz und liefert die Referenz auf das Duplikat zurück.
|
| ReferenceEquals
|
Vergleicht zwei Objektreferenzen und liefert einen booleschen Wert zurück, dem entnommen werden kann, ob die beiden Referenzen auf dasselbe Objekt zeigen.
|
| ToString
|
Liefert den voll qualifizierenden Namen einer Klasse.
|
Referenzvergleiche mit »Equals« und »ReferenceEquals«
Die beiden Methoden Equals und ReferenceEquals sind sich per Definition sehr ähnlich. Es werden zwei Objektvariablen miteinander verglichen, um festzustellen, ob beide dasselbe Objekt im Speicher referenzieren:
| Dim firstRef As New ClassA
|
| Dim secondRef As ClassA
|
| secondRef = firstRef
|
| Console.WriteLine(Object.Equals(firstRef, secondRef))
|
In diesem Codefragment wird die Referenz firstRef der Variablen secondRef zugewiesen. Beide Referenzen zeigen auf dasselbe konkrete Objekt, was der Aufruf der Equals-Methode bestätigt: Es wird True ausgegeben, was als referenzielle Identität der beiden Objektvariablen zu interpretieren ist. In diesem Fall können Sie sogar Equals gegen ReferenceEquals austauschen, am Ergebnis wird sich nichts ändern.
Sehen wir uns die Definition dieser beiden Methoden an, von denen Equals überladen ist:
| Public Overridable Function Equals(Object) As Boolean
|
| Public Shared Function Equals(Object, Object) As Boolean
|
| Public Shared Function ReferenceEquals(Object, Object) As Boolean
|
Die erste Variante der Equals-Methode ist als Instanzmethode, die zweite als Klassenmethode implementiert. Beachten Sie, dass die Instanzmethode Overridable gekennzeichnet ist und von jeder Klasse polymorph überschrieben werden kann. Die statische Equals-Variante ist nicht überschreibbar, ebenso die ähnlich lautende Methode ReferenceEquals. Damit ist auch garantiert, dass das Ergebnis des Aufrufs einer dieser beiden Methoden immer den Vergleich zwischen zwei Objektreferenzen liefert: Es ist True, wenn beide Referenzen auf ein dasselbe Objekt verweisen, andernfalls lautet das Ergebnis False.
Das Überschreiben von »Equals« und die Methode »GetHashCode«
Wollen Sie selbst die Equals-Methode überschreiben, sollten Sie immer eine wichtige Regel beachten:
|
Wird die Equals-Methode einer Klasse überschrieben, sollte auch die Methode GetHashCode überschrieben werden.
|
Schauen wir uns kurz die Definition dieser Methode an:
| Public Overridable Function GetHashCode() As Integer
|
Was aber leistet die Methode GetHashCode, und wozu dient sie?
Ein Hashcode ist eine Zahl vom Typ Integer, die ein bestimmtes Objekt eindeutig identifiziert. Diese Zahl wird auch als Schlüssel (key) bezeichnet, der immer zusammen mit einem Wert, beispielsweise einer Referenz, in Erscheinung tritt. Das Schlüssel-Wert-Paar wird in einer Hashtabelle verwaltet.
Jedes .NET-Objekt ist durch einen eindeutigen Schlüssel identifizierbar. Was hat das aber mit der Empfehlung zu tun, dass Sie als Entwickler GetHashCode überschreiben sollten, wenn Sie die Methode Equals überschreiben?
Standardmäßig benutzen wir Equals zum Referenzvergleich. Es könnte, wie Sie oben schon gesehen haben, natürlich auch ReferenceEquals sein oder – und genau das ist der entscheidende Punkt – auch über den Hashcode verglichen werden. Das folgende Beispiel beweist das. Es enthält eine Klasse mit einem parametrisierten Konstruktor, dem ein Integer übergeben wird. In der Main-Methode wird sowohl über Equals als auch über GetHashCode ein Vergleich gezogen, um zu bestimmen, ob zwei Variablen dasselbe Objekt referenzieren.
| ' ---------------------------------------------------------
|
| ' Beispiel: ...\Kapitel 10\HashCode
|
| ' --------------------------------------------------------
|
| Module Module1
|
| Sub Main()
|
| Dim obj1 As ClassA = New ClassA(5)
|
| Dim obj2 As ClassA = obj1
|
| ' Vergleich auf Basis des Hashcodes
|
| If (obj2.GetHashCode() = obj1.GetHashCode()) Then
|
| Console.WriteLine("Hashcode: Objekte sind gleich")
|
| End If
|
| ' Vergleich auf Basis der Equals-Methode
|
| If (obj2.Equals(obj1)) Then
|
| Console.WriteLine("Equals: Objekte sind gleich")
|
| End If
|
| Console.ReadLine()
|
| End Sub
|
| End Module
|
| Class ClassA
|
| Public MyProp As Integer
|
| Public Sub New(ByVal i As Integer)
|
| MyProp = i
|
| End Sub
|
| End Class
|
Die Ausgabe ist wie erwartet: Über beide Methodenaufrufe wird die Gleichheit festgestellt, da mit der Zuweisung der Objektvariablen obj1 an obj2 auch der Hashcode des Objekts obj1 gleichermaßen Berücksichtigung findet – wie alle anderen Felder des Objekts.
Equals vergleicht zwei Referenzen. Dem Überschreiben dieser Methode könnte die Idee zugrunde liegen, andere Vergleichskriterien für den betreffenden Typ heranzuziehen, beispielsweise der Vergleich bestimmter Feldinhalte. Für eine solche Klasse würde gelten, dass zwei Objektvariablen dieses Typs per Definition dann »gleich« sind, wenn sie sich in den Feldinhalten nicht unterscheiden.
Es könnte beispielsweise festgelegt werden, dass zwei Variablen mit gleichem Feldinhalt grundsätzlich immer dasselbe Objekt referenzieren sollen. Diese Technik, die der Schonung der Speicherressourcen dient, ist nicht neu. String-Objekte werden nach ähnlichen Kriterien erzeugt:
| Dim str1 As String = "Hallo Welt"
|
| Dim str2 As String = "Hallo Welt"
|
| Console.WriteLine(str1.GetHashCode())
|
| Console.WriteLine(str1.GetHashCode())
|
Beide Zeichenfolgen haben denselben Inhalt, referenzieren dieselbe Adresse im Hauptspeicher und zeigen demnach auf dasselbe konkrete Objekt.
Wir wollen nun die Aussage von Equals so definieren, dass zwei Objekte vom Typ ClassA dann als gleich angesehen werden, wenn ihre Eigenschaft MyProp jeweils denselben Inhalt aufweist. Mit dieser Vorgabe wollen wir die Klasse ClassA unseres Beispiels durch die überschriebene Methode Equals ergänzen:
| Class ClassA
|
| Public MyProp As Integer
|
| Public Sub New(ByVal i As Integer)
|
| MyProp = i
|
| End Sub
|
| Public Overrides Function Equals(ByVal obj As Object) As Boolean
|
| ' Prüfen, ob ein gültiges Objekt übergeben wurde und ob
|
| ' dieses auch dem Typ der Klasse entspricht
|
| If ((obj Is Nothing) Or (Not TypeOf obj Is ClassA)) Then
|
| Return False
|
| End If
|
| ' Rückgabewert des Feldvergleichs
|
| Return MyProp = obj.MyProp
|
| End Function
|
| End Class
|
Als Erstes wird in der Methode geprüft, ob ein gültiges Objekt über dem Parameter obj an die Methode übergeben wird und ob dieses auch dem Typ ClassA entspricht. Der letzte Vergleich ist notwendig, da der Parameter vom Typ Object ist und damit die Übergabe jedes x-beliebigen Typs erlaubt.
Sind beide Bedingungen erfüllt, kommt es mit
| Return MyProp = obj.MyProp
|
zu einem Vergleich der beiden Eigenschaften. Sind deren Inhalte identisch, liefert die Vergleichsoperation True zurück, weichen die Inhalte voneinander ab, ist der Rückgabewert False.
Um die Änderung zu testen, müssen wir nur noch ein wenig am Code in Main ändern. Wir wollen jetzt nicht mehr den Nachweis führen, dass ein konkretes Objekt, das über zwei Referenzen angesprochen werden kann, denselben Hashcode aufweist, sondern das Ziel soll lauten, den Nachweis zu führen, dass mit der Überschreibung der Methode Equals auch die Methode GetHashCode überschrieben werden soll.
| Module Module1
|
| Sub Main()
|
| Dim obj1 As ClassA = New ClassA(5)
|
| Dim obj2 As ClassA = New ClassA(5)
|
| ' Vergleich auf Basis des Hashcodes
|
| If (obj2.GetHashCode() = obj1.GetHashCode()) Then
|
| Console.WriteLine("Hashcode: Objekte sind gleich")
|
| Else
|
| Console.WriteLine("Hashcode: Objekte sind nicht gleich")
|
| End If
|
| ' Vergleich auf Basis der Equals-Methode
|
| If (obj2.Equals(obj1)) Then
|
| Console.WriteLine("Equals: Objekte sind gleich")
|
| Else
|
| Console.WriteLine("Equals: Objekte sind nicht gleich")
|
| End If
|
| Console.ReadLine()
|
| End Sub
|
| End Module
|
Der Vergleich mit der Equals-Methode liefert die Aussage, dass beide Objekte gleich sind, aber der Vergleich mit dem Hashcode schlägt natürlich fehl, weil beide Objekte mit unterschiedlichen Daten aufwarten.
Und genau in diesem Punkt liegt das Dilemma, wenn Sie zwar Equals, aber nicht GetHashCode überschreiben. Ein Benutzer Ihrer Klasse, der Objekte des Typs ClassA in einer Hashtabelle sammelt, wird vielleicht einen Vergleich anhand des Hashcodes ziehen und nicht mit Equals. Es kommt zu einer widersprüchlichen Aussage, die Klasse ist inkonsistent.
Es gehört nicht zu den trivialen Aufgaben, einen passenden Algorithmus zur Generierung des Hashcodes zu implementieren, wenn vom Standard abweichende Vergleichskriterien eine Rolle spielen. Es gibt zu diesem Thema einige gute, wenn auch langatmige Abhandlungen. Es wird jedoch immer ein Wert des Objekts zur Generierung des Hashcodes herangezogen – im einfachsten Fall der Inhalt einer Instanzvariablen des Objekts, z. B.:
| Public Class ClassA
|
| Dim intVar As Integer
|
| ' weiterer Klassencode
|
| Public Overrides Function GetHashCode() As Integer
|
| ' mathematische Operation mit intVar
|
| ' return Hashcode;
|
| End Function
|
| End Class
|
Das komplette Beispiel finden Sie auf der Buch-CD unter:
...\Kapitel 10\HashCodeOverriding
»ToString« und »GetType«
Wenden wir uns den beiden nächsten Methoden zu: ToString und GetType. Widmen wir uns zunächst der erstgenannten, die wir in den vergangenen Kapiteln schon oft benutzt haben:
| Public Overridable Function ToString() As String
|
ToString liefert per Definition eine Zeichenfolge zurück, die den voll qualifizierenden Namen der Klasse, also einschließlich der Angabe des Namespace, enthält. Viele Klassen überschreiben diese Methode, die dann einen anderen Rückgabewert hat. Sehen wir uns das an zwei Beispielen an:
| Dim strText As String = "Visual Basic 2005 ist spitze!"
|
| Console.WriteLine(strText.ToString())
|
| Dim intVar As Integer = 4711
|
| Console.WriteLine(intVar.ToString())
|
Die Ausgabe lautet:
| Visual Basic 2005 ist spitze!
|
und
Die Typen String und Integer überschreiben demnach ToString und liefern den Inhalt der Variablen, auf welche die Methode aufgerufen worden ist.
Mit GetType können Sie sich den Typ der Klasse besorgen, z. B.:
| Dim intVar As Integer = 10
|
| Console.WriteLine(intVar.GetType())
|
Jetzt wird nicht der Inhalt der Variablen, sondern der Datentyp ausgegeben. Der Typ des Rückgabewertes ist Type:
| Public Function GetType() As Type
|
Type liefert eine Referenz auf das Type-Objekt eines konkreten Objekts zurück. Dieses versetzt uns in die Lage, den Datentyp einer genaueren Analyse zu unterziehen.
Die Methode »MemberwiseClone«
Die Methode MemberwiseClone kopiert ein vorhandenes Objekt und liefert die Referenz auf die Kopie.
| Protected Function MemberwiseClone() As Object
|
Dabei werden alle Felder des Originals dupliziert: Felder, die auf Wertetypen basieren, werden bitweise kopiert, ebenso die auf Referenztypen basierenden Felder. Das bedeutet, dass die in einem Objekt referenzierten Subobjekte ihrerseits nicht dupliziert werden. Sowohl das Originalobjekt als auch die Kopie greifen auf dieselben Subobjekte zu.
MemberwiseClone ist geschützt deklariert und kann nur aus der aktuellen Instanz heraus aufgerufen werden. Sie müssen daher eine öffentliche Methode bereitstellen, auf die externer Code zugreifen kann. Dazu bietet die .NET-Klassenbibliothek mit ICloneable ein Interface an, dessen Sie sich bedienen sollten. Dieses Interface veröffentlicht nur die Methode Clone:
| Function Clone() As Object
|
Innerhalb einer zu duplizierenden Klasse wird Clone überschrieben. Clone ruft MemberwiseClone auf das aktuelle Objekt auf und liefert als Rückgabe die Referenz auf das Duplikat:
| Public Function Clone() As Object
|
| Return Me.MemberwiseClone()
|
| End Function
|
Etwas komplizierter wird es, wenn in einer Klasse Felder definiert sind, die auf Referenztypen basieren, und Sie gleichzeitig auch alle Subobjekte duplizieren möchten. Schauen Sie sich dazu die Implementierung der Klasse CloneableClass an:
| Public Class CloneableClass
|
| Implements ICloneable
|
| Public MyProp As Long
|
| Public internClass As New ClassA
|
| Public Function Clone() As Object _
|
| Implements ICloneable.Clone
|
| Return Me.MemberwiseClone()
|
| End Function
|
| End Class
|
Die Methode Clone wird an einen Aufrufer die Referenz auf eine Kopie zurückliefern. In der Kopie wird das Feld MyProp denselben Inhalt aufweisen wie das des Originals. Da MemberwiseClone alle Felder bitweise kopiert, referenziert die Eigenschaft internClass des Duplikats jedoch dasselbe Objekt wie das Original. Benötigen wir auch vom internen Objekt eine Kopie, haben wir zwei Möglichkeiten:
|
Wir erzeugen ein neues Objekt vom Typ ClassA und weisen diesem alle Eigenschaftswerte der Instanz internClass explizit zu. |
|
Wir hoffen, dass der Entwickler der Klasse ClassA weitsichtig genug war und die Klasse ClassA die Schnittstelle ICloneable-Methode implementiert. |
Wir wollen an einem Beispiel die unter Punkt 2 genannte Variante studieren. Das Objekt dupliziert sich selbst mit MemberwiseClone und liefert die Referenz der Kopie an den Aufrufer zurück. Exemplarisch nehmen wir die folgende Implementierung an:
| Public Class ClassA
|
| Implements ICloneable
|
| Public intVar As Integer
|
| Public Sub New()
|
| Dim rand As New Random
|
| intVar = rand.Next(0, 1000)
|
| End Sub
|
| Public Function Clone() As Object _
|
| Implements ICloneable.Clone
|
| Return Me.MemberwiseClone()
|
| End Function
|
| End Class
|
Beim Aufruf des parameterlosen Konstruktors wird nach dem Zufallsprinzip eine Zahl zwischen 0 und kleiner 1000 in der Instanzvariablen intVar festgehalten. Sie dient dazu, später den Erfolg des Klonens zu beweisen.
Nun können wir uns noch einmal der Klasse CloneableClass zuwenden und die Methode Clone gemäß unseren Anforderungen überarbeiten.
| Public Class CloneableClass
|
| Implements ICloneable
|
| Public MyProp As Long
|
| Public internClass As New ClassA
|
| Public Function Clone() As Object _
|
| Implements ICloneable.Clone
|
| Dim internObj As CloneableClass
|
| internObj = Me.MemberwiseClone()
|
| internObj.internClass = Me.internClass.Clone()
|
| Return internObj
|
| End Function
|
| End Class
|
Zuerst wird in der Methode Clone die Variable internObj vom Typ CloneableClass deklariert und dieser über MemberwiseClone die Referenz auf das Duplikat zugewiesen:
| internObj = Me.MemberwiseClone()
|
Die Kopie referenziert noch dasselbe interne Objekt wie das Original. Vom Objekt internClass besorgen wir uns deshalb eine Kopie und weisen diese dem Feld internClass der Klone zu.
Damit haben wir unser Ziel erreicht. Was uns jetzt noch fehlt, ist die Bestätigung unserer Überlegungen. Dazu benutzen wir die Methode GetHashCode, da sich jedes Objekt durch einen eigenen Hashcode identifizieren lässt.
| ' ----------------------------------------------------------
|
| ' Beispiel: ...\Kapitel 10\MemberwiseClonen
|
| ' ----------------------------------------------------------
|
| Module Module1
|
| Sub Main()
|
| Dim obj As New CloneableClass
|
| Dim dupli As CloneableClass = obj.Clone()
|
| Console.WriteLine("Hashcode(obj) = {0}", _
|
| obj.GetHashCode())
|
| Console.WriteLine("Hashcode(dupli) = {0}", _
|
| dupli.GetHashCode())
|
| Console.WriteLine("Hashcode(obj.internClass) = {0}", _
|
| obj.internClass.GetHashCode())
|
| Console.WriteLine("Hashcode(dupli.internClass) = {0}", _
|
| dupli.internClass.GetHashCode())
|
| Console.WriteLine("intVar(obj) = {0}", _
|
| obj.internClass.intVar)
|
| Console.WriteLine("intVar (dupli) = {0}", _
|
| dupli.internClass.intVar)
|
| Console.ReadLine()
|
| End Sub
|
| End Module
|
Der Testcode könnte an der Konsole zur folgenden Anzeige führen:
| Hashcode(obj) = 70
|
| Hashcode(dupli) = 72
|
| Hashcode(obj.internClass) = 73
|
| Hashcode(dupli.internClass) = 74
|
| internVar(obj) = 768
|
| internVar(dupli) = 768
|
Die Hashcodes der Referenzen dupli und obj sind unterschiedlich, ebenso die des enthaltenen Feldes vom Typ ClassA. Daraus kann der Schluss gezogen werden, dass das ClassA-Feld des Originals ebenfalls geklont worden ist.
|